package cmd

import (
	"fmt"
	"gitee.com/whilew/gmodel/internal/cfg"
	"gitee.com/whilew/gmodel/internal/util"
	"github.com/spf13/cobra"
	"os"
	"path/filepath"
	"runtime"
	"sort"
	"strings"
)

var (
	envW = envCmd.Flags().BoolP("write", "w", false, "")
	envU = envCmd.Flags().BoolP("unset", "u", false, "")
	envG = envCmd.Flags().BoolP("global", "g", false, "")
)

var envCmd = &cobra.Command{
	Use:   "env",
	Short: "print gmodel environment information",
	Long: `Env prints gmodel environment information.

By default env prints information as a shell script
(on Windows, a batch file). If one or more variable
names is given as arguments, env prints the value of
each named variable on its own line.

The -w flag requires one or more arguments of the
form NAME=VALUE and changes the default settings
of the named environment variables to the given values.
	`,
}

func init() {
	envCmd.Run = envRun
	rootCmd.AddCommand(envCmd)
}

func envRun(cmd *cobra.Command, args []string) {
	if *envU && *envW {
		util.Fatalf("gmodel: cannot use -u with -w")
	}

	if *envG && !(*envW || *envU) {
		util.Fatalf("gmodel: cannot use -g without -w or -u")
	}

	if *envW {
		runEnvW(args)
		return
	}

	if *envU {
		runEnvU(args)
		return
	}

	runPrintEnv()
}

func runEnvW(args []string) {
	if len(args) == 0 {
		util.Fatalf("gmodel: no KEY=VALUE arguments given")
	}

	add := make(map[string]string)
	for _, arg := range args {
		i := strings.Index(arg, "=")
		if i < 0 {
			util.Fatalf("gmodel: arguments must be KEY=VALUE: invalid argument: %s", arg)
		}
		key, val := arg[:i], arg[i+1:]
		if _, ok := add[key]; ok {
			util.Fatalf("gmodel: multiple values for key: %s", key)
		}
		add[key] = val
	}
	updateEnvFile(add, nil)
}
func runEnvU(args []string) {
	if len(args) == 0 {
		util.Fatalf("gmodel: no KEY arguments given")
	}

	del := make(map[string]bool)
	for _, arg := range args {
		del[arg] = true
	}

	updateEnvFile(nil, del)
}
func runPrintEnv() {
	kv, err := cfg.GetEnv()
	if err != nil {
		util.Fatalf("gmodel: %v", err)
	}
	for k, v := range kv {
		switch runtime.GOOS {
		default:
			fmt.Fprintf(os.Stdout, "%s=\"%s\"\n", k, v)
		case "plan9":
			if strings.IndexByte(v, '\x00') < 0 {
				fmt.Fprintf(os.Stdout, "%s='%s'\n", k, strings.ReplaceAll(v, "'", "''"))
			} else {
				v := strings.Split(v, "\x00")
				fmt.Fprintf(os.Stdout, "%s=(", k)
				for x, s := range v {
					if x > 0 {
						fmt.Fprintf(os.Stdout, " ")
					}
					fmt.Fprintf(os.Stdout, "%s", s)
				}
				fmt.Fprintf(os.Stdout, ")\n")
			}
		case "windows":
			fmt.Fprintf(os.Stdout, "set %s=%s\n", k, v)
		}
	}
}

func updateEnvFile(add map[string]string, del map[string]bool) {
	file, err := cfg.EnvFile(*envG)
	if err != nil {
		util.Fatalf("gmodel: cannot find gomodel env config: %v", err)
	}
	data, err := os.ReadFile(file)
	if err != nil && (!os.IsNotExist(err) || len(add) == 0) {
		util.Fatalf("gmodel: reading gmodel env config: %v", err)
	}

	lines := strings.SplitAfter(string(data), "\n")
	if lines[len(lines)-1] == "" {
		lines = lines[:len(lines)-1]
	} else {
		lines[len(lines)-1] += "\n"
	}

	prev := make(map[string]int)
	for l, line := range lines {
		if key := lineToKey(line); key != "" {
			if p, ok := prev[key]; ok {
				lines[p] = ""
			}
			prev[key] = l
		}
	}

	// Add variables (go env -w). Update existing lines in file if present, add to end otherwise.
	for key, val := range add {
		if p, ok := prev[key]; ok {
			lines[p] = key + "=" + val + "\n"
			delete(add, key)
		}
	}
	for key, val := range add {
		lines = append(lines, key+"="+val+"\n")
	}

	// Delete requested variables (go env -u).
	for key := range del {
		if p, ok := prev[key]; ok {
			lines[p] = ""
		}
	}

	// Sort runs of KEY=VALUE lines
	// (that is, blocks of lines where blocks are separated
	// by comments, blank lines, or invalid lines).
	start := 0
	for i := 0; i <= len(lines); i++ {
		if i == len(lines) || lineToKey(lines[i]) == "" {
			sortKeyValues(lines[start:i])
			start = i + 1
		}
	}

	data = []byte(strings.Join(lines, ""))
	err = os.WriteFile(file, data, 0666)
	if err != nil {
		// Try creating directory.
		os.MkdirAll(filepath.Dir(file), 0777)
		err = os.WriteFile(file, data, 0666)
		if err != nil {
			util.Fatalf("gmodel: writing go env config: %v", err)
		}
	}
}

// lineToKey returns the KEY part of the line KEY=VALUE or else an empty string.
func lineToKey(line string) string {
	i := strings.Index(line, "=")
	if i < 0 || strings.Contains(line[:i], "#") {
		return ""
	}
	return line[:i]
}

// sortKeyValues sorts a sequence of lines by key.
// It differs from sort.Strings in that keys which are GOx where x is an ASCII
// character smaller than = sort after GO=.
// (There are no such keys currently. It used to matter for GO386 which was
// removed in Go 1.16.)
func sortKeyValues(lines []string) {
	sort.Slice(lines, func(i, j int) bool {
		return lineToKey(lines[i]) < lineToKey(lines[j])
	})
}
